4. The Freehand Tool
Our final tool, the
Freehand tool, lets you create a Bézier path. With this tool, you can
draw a big string of curved sections by tapping and dragging. Each touch
defines a new point on the curve, and dragging immediately after the
touch lets you define control points that determine the curvature around
that point. While you're dragging a control point around, it's shown
with a dashed red line connecting it to the last point you touched, just
to make it stand out a little more. To finish off a path, touch the
Freehand button in the toolbar (or any other tool button, for that
matter). This Freehand tool corresponds to what most people think of as
Bézier curves (if they're thinking of Bézier curves at all).
Due to the additional
complexity of the interaction with the Freehand tool, we're not going to
consider the use of multitouch here. The Freehand tool relies on making
a series of points by touching and releasing multiple times, and if we
were to track multiple touches, it would be impossible to guess which
subsequent touch belonged with which previous touch. Instead, we have a
different set of instance variables that are used to hold the state of
the current in-progress curve segment, if there is one. As each curve
segment is created, it's added to a workingPath object, which is finally sent to the delegate when it's done.
Create a new class called FreehandTool, with the following code:
// FreehandTool.h
#import <Foundation/Foundation.h>
#import "Tool.h"
@interface FreehandTool : NSObject <Tool> {
id <ToolDelegate> delegate;
UIBezierPath *workingPath;
CGPoint nextSegmentPoint1;
CGPoint nextSegmentPoint2;
CGPoint nextSegmentCp1;
CGPoint nextSegmentCp2;
BOOL isDragging;
BOOL settingFirstPoint;
}
@property (retain, nonatomic) UIBezierPath *workingPath;
+ (FreehandTool *)sharedFreehandTool;
@end
// FreehandTool.m
#import "FreehandTool.h"
#import "PathDrawingInfo.h"
#import "SynthesizeSingleton.h"
@implementation FreehandTool
@synthesize delegate, workingPath;
SYNTHESIZE_SINGLETON_FOR_CLASS(FreehandTool);
- init {
if ((self = [super init])) {
}
return self;
}
- (void)activate {
self.workingPath = [UIBezierPath bezierPath];
settingFirstPoint = YES;
}
- (void)deactivate {
// this is where we finally tell about our path
PathDrawingInfo *info = [PathDrawingInfo pathDrawingInfoWithPath:self.workingPath fillColor:delegate.fillColor strokeColor:delegate.strokeColor];
[delegate addDrawable:info];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
isDragging = YES;
UIView *touchedView = [delegate viewForUseWithTool:self];
UITouch *touch = [[event allTouches] anyObject];
CGPoint touchPoint = [touch locationInView:touchedView];
// set nextSegmentPoint2
nextSegmentPoint2 = touchPoint;
// establish nextSegmentCp2
nextSegmentCp2 = touchPoint;
if (workingPath.empty) {
// this is the first touch in a path, so set the "1" variables as well
nextSegmentCp1 = touchPoint;
nextSegmentPoint1 = touchPoint;
[workingPath moveToPoint:touchPoint];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
isDragging = NO;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
isDragging = NO;
UIView *touchedView = [delegate viewForUseWithTool:self];
UITouch *touch = [[event allTouches] anyObject];
CGPoint touchPoint = [touch locationInView:touchedView];
nextSegmentCp2 = touchPoint;
// complete segment and add to list
if (settingFirstPoint) {
// the first touch'n'drag doesn't complete a segment, we just
// note the change of state and move along
settingFirstPoint = NO;
} else {
// nextSegmentCp2, which we've been dragging around, is translated
// around nextSegmentPoint2 for creation of this segment.
CGPoint shiftedNextSegmentCp2 = CGPointMake(
nextSegmentPoint2.x + (nextSegmentPoint2.x - nextSegmentCp2.x),
nextSegmentPoint2.y + (nextSegmentPoint2.y - nextSegmentCp2.y));
[workingPath addCurveToPoint:nextSegmentPoint2 controlPoint1:nextSegmentCp1 controlPoint2:shiftedNextSegmentCp2];
// the "2" values are now copied to the "1" variables
nextSegmentPoint1 = nextSegmentPoint2;
nextSegmentCp1 = nextSegmentCp2;
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UIView *touchedView = [delegate viewForUseWithTool:self];
UITouch *touch = [[event allTouches] anyObject];
CGPoint touchPoint = [touch locationInView:touchedView];
if (settingFirstPoint) {
nextSegmentCp1 = touchPoint;
} else {
// adjust nextSegmentCp2
nextSegmentCp2 = touchPoint;
}
}
- (void)drawTemporary {
// draw all the segments we've finished so far
[workingPath stroke];
if (isDragging) {
// draw the current segment that's being created
if (settingFirstPoint) {
// just draw a line
UIBezierPath *currentWorkingSegment = [UIBezierPath bezierPath];
[currentWorkingSegment moveToPoint:nextSegmentPoint1];
[currentWorkingSegment addLineToPoint:nextSegmentCp1];
[[delegate strokeColor] setStroke];
[currentWorkingSegment stroke];
} else {
// nextSegmentCp2, which we've
// been dragging around, is translated around nextSegmentPoint2
// for creation of this segment
CGPoint shiftedNextSegmentCp2 = CGPointMake(
nextSegmentPoint2.x + (nextSegmentPoint2.x - nextSegmentCp2.x),
nextSegmentPoint2.y + (nextSegmentPoint2.y - nextSegmentCp2.y));
UIBezierPath *currentWorkingSegment = [UIBezierPath bezierPath];
[currentWorkingSegment moveToPoint:nextSegmentPoint1];
[currentWorkingSegment addCurveToPoint:nextSegmentPoint2 controlPoint1:nextSegmentCp1 controlPoint2:shiftedNextSegmentCp2];
[[delegate strokeColor] setStroke];
[currentWorkingSegment stroke];
}
}
if (!CGPointEqualToPoint(nextSegmentCp2, nextSegmentPoint2) && !settingFirstPoint) {
// draw the guideline to the next segment
UIBezierPath *currentWorkingSegment = [UIBezierPath bezierPath];
[currentWorkingSegment moveToPoint:nextSegmentCp2];
CGPoint shiftedNextSegmentCp2 = CGPointMake(
nextSegmentPoint2.x + (nextSegmentPoint2.x - nextSegmentCp2.x),
nextSegmentPoint2.y + (nextSegmentPoint2.y - nextSegmentCp2.y));
[currentWorkingSegment addLineToPoint:shiftedNextSegmentCp2];
To display the temporary
curve that the user is dragging around, we will use a dashed line
instead of a solid line, to help make it stand out from the background.
This dash pattern specifies that the line will be drawn with 10 pixels
in the stroke color we set, then skip 7 pixels, repeating forever.
float dashPattern[] = {10.0, 7.0};
[currentWorkingSegment setLineDash:dashPattern count:2 phase:0.0];
[[UIColor redColor] setStroke];
[currentWorkingSegment stroke];
}
}
- (void)dealloc {
self.workingPath = nil;
self.delegate = nil;
[super dealloc];
}
@end
Now add a few lines to DudelViewController.m to bring it together:
#import "FreehandTool.h"
- (IBAction)touchFreehandItem:(id)sender {
self.currentTool = [FreehandTool sharedFreehandTool];
[freehandButton setImage:[UIImage imageNamed:@"button_bezier_selected.png"]];
}
That's it! The final drawing tool button in our toolbar is complete. Try it out and see how it works. Figure 4 shows a bit of "art" created with our Dudel tools.